import tkinter as tk
from tkinter import filedialog, messagebox
from tkinter import scrolledtext, font
import threading
import queue
import os
import io
import sys
import jlink_operations
from stack_tracer import StackTracer  # 确保正确导入你的类


class AnalysisApp:
    def __init__(self, root_var):
        self.root = root_var
        root.minsize(1024, 500)  # 根据您的需求设置合适的宽高
        # 创建菜单栏
        self.create_menu()
        # 配置根窗口，使其能够自动调整大小
        root.columnconfigure(0, weight=0)  # 标签不动
        root.columnconfigure(1, weight=1)  # 输入框扩展
        root.columnconfigure(2, weight=0)  # 浏览按钮不动
        root.rowconfigure(0, weight=0)
        root.rowconfigure(1, weight=0)
        root.rowconfigure(2, weight=0)
        root.rowconfigure(3, weight=0)
        root.rowconfigure(4, weight=0)
        root.rowconfigure(5, weight=0)
        root.rowconfigure(6, weight=1)  # 使日志输出区域自适应扩展

        # DLL文件选择

        self.dll_path = tk.StringVar()
        tk.Label(root, text="选择DLL文件:", anchor="w").grid(row=0, column=0, sticky="w", padx=10, pady=5)
        tk.Entry(root, textvariable=self.dll_path, width=50).grid(row=0, column=1, sticky="we", padx=10, pady=5)
        tk.Button(root, text="浏览", command=self.load_dll).grid(row=0, column=2, padx=10, pady=5)

        self.prj_path = tk.StringVar()
        tk.Label(root, text="选择工程目录:", anchor="w").grid(row=1, column=0, sticky="w", padx=10, pady=5)
        tk.Entry(root, textvariable=self.prj_path, width=50).grid(row=1, column=1, sticky="we", padx=10, pady=5)
        tk.Button(root, text="打开", command=self.load_prj).grid(row=1, column=2, padx=10, pady=5)

        """
        # AXF文件选择
        self.axf_path = tk.StringVar()
        tk.Label(root, text="选择.AXF文件:", anchor="w").grid(row=1, column=0, sticky="w", padx=10, pady=5)
        tk.Entry(root, textvariable=self.axf_path, width=50).grid(row=1, column=1, sticky="we", padx=10, pady=5)
        tk.Button(root, text="浏览", command=self.load_axf).grid(row=1, column=2, padx=10, pady=5)

        # MAP文件选择
        self.map_path = tk.StringVar()
        tk.Label(root, text="选择.MAP文件:", anchor="w").grid(row=2, column=0, sticky="w", padx=10, pady=5)
        tk.Entry(root, textvariable=self.map_path, width=50).grid(row=2, column=1, sticky="we", padx=10, pady=5)
        tk.Button(root, text="浏览", command=self.load_map).grid(row=2, column=2, padx=10, pady=5)
        """
        # 起始地址和分析长度输入
        self.start_address = tk.StringVar(value="0x20000000")
        self.length = tk.StringVar(value="0x4000")

        # 起始地址标签和输入框
        tk.Label(root, text="分析起始地址:", anchor="w").grid(row=3, column=0, sticky="w", padx=10, pady=5)
        tk.Entry(root, textvariable=self.start_address, width=15).grid(row=3, column=1, sticky="w", padx=10, pady=5)

        # 分析长度标签和输入框
        tk.Label(root, text="分析长度:", anchor="w").grid(row=4, column=0, sticky="w", padx=10, pady=5)
        tk.Entry(root, textvariable=self.length, width=15).grid(row=4, column=1, sticky="w", padx=10, pady=5)

        # 将提取和分析按钮放在同一列中，使用padx和pady设置偏移量
        tk.Button(root, text="提取", command=self.start_analysis).grid(row=3, column=1, padx=(130, 0), pady=(5, 0), sticky="w")
        tk.Button(root, text="分析", command=self.analyze_stack_info).grid(row=4, column=1, padx=(130, 0), pady=(5, 5), sticky="w")

        # 日志窗口
        tk.Label(root, text="日志输出:", anchor="w").grid(row=5, column=0, sticky="nw", padx=10, pady=5)
        tk.Button(root, text="清空", command=self.clear_log).grid(row=5, column=2, padx=10, pady=5, sticky="e")
        custom_font = font.Font(family="Consolas", size=11)  # 设置字体样式和大小
        self.log_window = scrolledtext.ScrolledText(root, width=60, height=15, state="disabled", font=custom_font)
        self.log_window.grid(row=6, column=0, columnspan=3, sticky="nsew", padx=10, pady=10)

        # 设置日志窗口的纵向权重，以便在窗口调整大小时自适应
        root.rowconfigure(6, weight=1)

        # 用于接收子线程日志的队列
        self.log_queue = queue.Queue()

        # 定时检查日志队列并更新日志窗口
        self.root.after(100, self.process_log_queue)

        # 在关闭窗口时调用 on_closing 方法
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

        # 用于管理子进程
        self.processes = []  # 用于保存子进程

        self.root.title("分析程序")

        if getattr(sys, 'frozen', False):
            # 如果是打包后的可执行文件，使用sys._MEIPASS获取文件路径
            current_dir = sys._MEIPASS  # type: ignore
        else:
            # 如果是源代码，使用os.path获取文件路径
            current_dir = os.path.dirname(os.path.abspath(__file__))

        icon_path = os.path.join(current_dir, 'favicon.ico')
        self.root.iconbitmap(icon_path)
        self.show_about()  # 在程序启动时显示关于信息

    def create_menu(self):
        menubar = tk.Menu(self.root)
        menubar.add_command(label="关于", command=self.show_about)  # 直接添加关于选项
        self.root.config(menu=menubar)


    def show_about(self):
        messagebox.showinfo("关于", "版本: 1.0\n作者: 叶大鹏, 谭继鑫\n版权: PN学堂\n网址：www.pnxuetang.cn")

    def load_prj(self):
        folder_dir = filedialog.askdirectory()
        if dir:  # 如果选择了目录
            self.prj_path.set(folder_dir)  # 将目录路径设置到文本框中

    def load_dll(self):
        path = filedialog.askopenfilename(filetypes=[("DLL files", "*.dll")])
        if path:
            self.dll_path.set(path)
            self.log("加载DLL文件: " + path)

    """
    def load_axf(self):
        path = filedialog.askopenfilename(filetypes=[("AXF files", "*.axf")])
        if path:
            self.axf_path.set(path)
            self.log("加载AXF文件: " + path)

    def load_map(self):
        path = filedialog.askopenfilename(filetypes=[("map files", "*.map")])
        if path:
            self.map_path.set(path)
            self.log("加载MAP文件: " + path)
    """

    def find_files_with_extension(self, directory, extension):
        if not os.path.isdir(directory):
            print(f"目录不存在: {directory}")
            return

        found = False  # 用于跟踪是否找到文件
        for rooter, dirs, files in os.walk(directory):
            for file in files:
                if file.endswith(extension):
                    # print(os.path.join(rooter, file))
                    self.log(os.path.join(rooter, file))
                    found = True
                    return os.path.join(rooter, file)

        if not found:
            print(f"没有找到扩展名为 {extension} 的文件。")

    def start_analysis(self):
        # 获取用户输入的值
        dll_path = self.dll_path.get()
        # axf_path = self.axf_path.get()
        axf_path = self.find_files_with_extension(self.prj_path.get(), '.axf')
        # map_path = self.map_path.get()
        map_path = self.find_files_with_extension(self.prj_path.get(), '.map')
        start_address = self.start_address.get()
        length = self.length.get()

        # 添加设备名称输入
        # device_name = self.device_name.get()  # 获取设备名称
        device_name = "GD32F303ZE"
        # 输入验证
        if not all([dll_path, axf_path, map_path, start_address, length]):
            messagebox.showwarning("警告", "所有字段都必须填写。")
            return

        # 创建线程来运行分析脚本并捕获输出
        thread = threading.Thread(target=self.run_analysis_script,
                                  args=(dll_path, axf_path, map_path, start_address, length, device_name))
        thread.start()
        # jlink_operations.run_jlink_script(dll_path, axf_path, start_address, length, device_name)

    def run_analysis_script(self, dll_path, axf_path, map_path, start_address, length, device_name):
        # 使用 StringIO 重定向 stdout 和 stderr
        output_buffer = io.StringIO()
        error_buffer = io.StringIO()
        sys.stdout = output_buffer  # 重定向 stdout
        sys.stderr = error_buffer  # 重定向 stderr

        try:
            # 调用 jlink_operations 中的方法
            jlink_operations.run_jlink_script(dll_path, axf_path, start_address, length, device_name)

            # 获取输出并放入日志队列
            output = output_buffer.getvalue()
            # error_output = error_buffer.getvalue()

            # 处理输出，去掉每行开头的"Error:"前缀
            for line in output.splitlines():
                if line.startswith("Error:"):
                    line = line[len("Error:"):].lstrip()  # 去掉"Error:"前缀和任何前导空格
                self.log(line)
                # self.log_queue.put(output + error_output)  # 合并输出
            self.log("提取完成")
        except Exception as e:
            self.log(f"运行分析脚本时出错: {str(e)}")
        finally:
            sys.stdout = sys.__stdout__  # 恢复 stdout
            sys.stderr = sys.__stderr__  # 恢复 stderr

    def analyze_stack_info(self):
        """执行分析栈信息的 Python 脚本"""
        self.log("\nStack trace analysis begins")
        dll_path = self.dll_path.get()
        # axf_file = self.axf_path.get()  # 从用户输入获取 AXF 文件路径
        axf_file = self.find_files_with_extension(self.prj_path.get(), '.axf')
        axf_directory = os.path.dirname(axf_file)
        # 将 bin_file_path 指向 axf_directory 同目录下的 ram.bin
        bin_file = os.path.join(axf_directory, 'ram.bin')
        reg_dump_file = os.path.join(axf_directory, 'registers.txt')
        # map_file = self.map_path.get()  # 从用户输入获取 MAP 文件路径
        map_file = self.find_files_with_extension(self.prj_path.get(), '.map')

        if not all([dll_path, axf_file, map_file]):
            messagebox.showwarning("警告", "前三行字段都必须填写。")
            return

        # 创建线程来运行分析脚本
        thread = threading.Thread(target=self.execute_python_script, args=(axf_file, bin_file),
                                  kwargs={'reg_dump': reg_dump_file, 'map_file': map_file})
        thread.start()

    def execute_python_script(self, axf_file, bin_file, reg_dump=None, map_file=None):
        # 使用 StringIO 重定向 stdout 和 stderr
        output_buffer = io.StringIO()
        error_buffer = io.StringIO()
        sys.stdout = output_buffer  # 重定向 stdout
        sys.stderr = error_buffer  # 重定向 stderr
        try:
            tracer = StackTracer(axf_file, bin_file, reg_dump, map_file)
            tracer.analyze()  # 调用分析方法
            output = output_buffer.getvalue()
            error_output = error_buffer.getvalue()
            self.log_queue.put(output + error_output)  # 合并输出
            self.log_queue.put("分析完成")
        except Exception as e:
            self.log("分析出错: " + str(e))
        finally:
            sys.stdout = sys.__stdout__  # 恢复 stdout
            sys.stderr = sys.__stderr__  # 恢复 stderr

    def read_process_output(self, process):
        for line in process.stdout:
            self.log_queue.put(line.strip())
        for line in process.stderr:
            self.log_queue.put(line.strip())

        # 子进程结束后从列表中移除
        self.processes.remove(process)

    def process_log_queue(self):
        """定时检查日志队列并更新日志窗口"""
        try:
            while True:  # 从队列中获取所有消息
                message = self.log_queue.get_nowait()
                self.log(message)
        except queue.Empty:
            pass
        self.root.after(100, self.process_log_queue)

    def log(self, message):
        """在日志窗口中添加信息"""
        # 开启日志窗口的可编辑状态
        self.log_window.config(state="normal")

        # 按原样插入子程序输出内容，确保格式保持不变
        self.log_window.insert(tk.END, message + "\n")

        # 禁用编辑状态并保持滚动到最底部
        self.log_window.config(state="disabled")
        self.log_window.yview(tk.END)

    def on_closing(self):
        """关闭窗口时，终止所有子进程并退出程序"""
        for process in self.processes:
            process.terminate()  # 尝试优雅终止
            process.wait()  # 等待进程结束

        self.root.destroy()  # 销毁主窗口

    def clear_log(self):
        """清空日志窗口内容"""
        self.log_window.config(state="normal")
        self.log_window.delete("1.0", tk.END)
        self.log_window.config(state="disabled")


if __name__ == "__main__":
    root = tk.Tk()
    app = AnalysisApp(root)
    # root.iconbitmap("favicon.ico")  # 设置窗口图标（.ico格式）
    root.mainloop()
